package com.nulldev.util.io.file;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
import java.util.List;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.RandomUtil;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Charsets.CharsetUtil;
import com.nulldev.util.internal.backport.concurrency9.Lists;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;

public class FileUtil {
	private static final Logger log = LoggerFactory.getLogger();
	public static final String HOME_DIRECTORY = System.getProperty("user.home");
	public static final boolean ENABLE_STREAM_BUFFERING = Arguments.notArguments("--FileUtil.disableStreamBuffering");

	public static boolean isFile(final String string) {
		Variables.requireNonNull(string, "string");
		try {
			return new File(string).exists();
		} catch (Exception e) {
			return false;
		}
	}

	public static File safeCreate(final String location) {
		Variables.requireNonNull(location, "location");
		try {
			return new File(location);
		} catch (Exception e) {
			JVM.errorln(e);
			return null;
		}
	}

	public static List<String> readFile(final File file) throws Exception {
		return readFile(file, CharsetUtil.UTF_8);
	}

	public static List<String> readFile(final File file, final Charset charset) throws Exception {
		Variables.requireNonNull(file, "file");
		final List<String> data = FastLists.list();
		if (file.isDirectory()) {
			throw new Exception("File is a directory.");
		}
		final InputStream fis = new FileInputStream(file);
		final InputStreamReader isr = new InputStreamReader(fis, Variables.requireNonNullObject(charset, "charset"));
		final BufferedReader br = new BufferedReader(isr, IOUtils.MEMORY_ALLOC_BUFFER);
		String buffer;
		while ((buffer = br.readLine()) != null) {
			data.add(buffer);
		}
		try {
			fis.close();
			isr.close();
			br.close();
		} catch (Exception e) {
		}
		return data;
	}

	public static String readFileToString(final File file) {
		return readFileToString(file, CharsetUtil.UTF_8);
	}

	public static String readFileToString(final File file, final Charset charset) {
		Variables.requireNonNull(file, "file");
		try {
			final List<String> lines = readFile(file, charset);
			final StringBuilder builder = new StringBuilder(IOUtils.STRING_ALLOC_BUFFER);
			for (int i = 0; i < lines.size(); i++) {
				final String str = lines.get(i);
				if (i == lines.size() - 1) {
					builder.append(str);
				} else {
					builder.append(str + "\n");
				}
			}

			return builder.toString();
		} catch (Exception e) {
			JVM.errorln(e);
			return "readFileToString.Error";
		}
	}

	public static File createTempFile(final String suff) {
		Variables.requireNonNull(suff, "suff");
		try {
			final File t = File.createTempFile("_temp_" + RandomUtil.randInt(1, Integer.MAX_VALUE), (suff.startsWith(".") ? suff : "." + suff));
			t.deleteOnExit();
			return t;
		} catch (Exception e) {
			JVM.errorln(e);
			return createTempFile(suff);
		}
	}

	public static void writeFile(final File file, final String data) throws Exception {
		Variables.requireNonNull(file, "file");
		Variables.requireNonNull(data, "data");
		if (file.isDirectory()) {
			throw new Exception("File is a directory.");
		}
		if (!file.exists()) {
			file.createNewFile();
		}
		final PrintWriter writer = new PrintWriter(file, "UTF-8");
		writer.println(data);
		writer.close();
	}

	public static void writeFile(final File file, final byte[] data) throws IOException {
		Variables.requireNonNull(file, "file");
		Variables.requireNonNull(data, "data");

		if (file.isDirectory()) {
			throw new IOException("File is a directory.");
		}

		if (!file.exists()) {
			file.createNewFile();
		}

		final FileOutputStream fos = new FileOutputStream(file);
		fos.write(data);
		fos.flush();
		fos.close();
	}

	private static final boolean ENABLE_FAST_COPY = IOUtils.canUseFastCopy();

	public static byte[] getResourceBytes(final Class<?> cls, final String resourcePath) {
		Variables.requireNonNull(cls, "cls");
		Variables.requireNonNull(resourcePath, "resourcePath");
		try {
			InputStream in = cls.getResourceAsStream(resourcePath);
			if (in == null) {
				log.error("getResourceBytes(file) -> File not found! Returning null! [rp=" + resourcePath + "]");
				return null;
			}
			final int av = in.available();
			final ByteArrayOutputStream baos = new ByteArrayOutputStream(av <= 0 ? IOUtils.MEMORY_ALLOC_BUFFER : av);
			if (ENABLE_FAST_COPY) {
				IOUtils.fastCopy(in, baos);
			} else {
				IOUtils.copy(in, baos);
			}
			IOUtils.closeQuietly(in);
			final byte[] out = baos.toByteArray();
			baos.close();

			return out;
		} catch (Exception e) {
			log.error("getResourceBytes(file) -> Failed to read resource!", e);
			return null;
		}
	}

	public static Path getFileURIFromResource(final Class<?> cls, final String fileName) throws Exception {
		Variables.requireNonNull(cls, "cls");
		Variables.requireNonNull(fileName, "fileName");
		final ClassLoader classLoader = cls.getClassLoader();
		return Paths.get(classLoader.getResource(fileName).getPath());
	}

	public static String getResourceStringNIO(final Class<?> cls, final String fileName) throws Exception {
		Variables.requireNonNull(cls, "cls");
		Variables.requireNonNull(fileName, "fileName");
		return getResourceStringNIO(getFileURIFromResource(cls, fileName));
	}

	public static String getResourceStringNIO(final Path p) throws Exception {
		Variables.requireNonNull(p, "p");
		CharBuffer charBuffer = null;

		final FileChannel fileChannel = (FileChannel) Files.newByteChannel(p, EnumSet.of(StandardOpenOption.READ));
		final MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

		if (mappedByteBuffer != null) {
			charBuffer = Charset.forName("UTF-8").decode(mappedByteBuffer);
			final String out = charBuffer.toString();
			IOUtils.closeQuietly(fileChannel);
			mappedByteBuffer.clear();
			return out;
		} else {
			IOUtils.closeQuietly(fileChannel);
			return null;
		}
	}

	public static InputStream getResourceStream(final Class<?> cls, final String resourcePath) {
		Variables.requireNonNull(cls, "cls");
		Variables.requireNonNull(resourcePath, "resourcePath");
		try {
			InputStream in = cls.getResourceAsStream(resourcePath);
			if (in == null) {
				log.error("getResourceStream(file) -> File not found! Returning null! [rp=" + resourcePath + "]");
				return null;
			}
			if (ENABLE_STREAM_BUFFERING)
				return new BufferedInputStream(in, IOUtils.MEMORY_ALLOC_BUFFER);
			else
				return in;
		} catch (Exception e) {
			log.error("getResourceStream(file) -> Failed to read resource!", e);
			return null;
		}
	}

	public static File getResourceAsFile(final Class<?> cls, final String resourcePath) {
		Variables.requireNonNull(cls, "cls");
		Variables.requireNonNull(resourcePath, "resourcePath");
		try {
			final InputStream in = cls.getResourceAsStream(resourcePath);
			if (in == null) {
				log.error("getResourceAsFile(file) -> File not found! Returning null!");
				return null;
			}
			final File tempFile = File.createTempFile(String.valueOf(in.hashCode()), ".tmp");
			tempFile.deleteOnExit();
			final FileOutputStream out = new FileOutputStream(tempFile);
			if (ENABLE_FAST_COPY) {
				IOUtils.fastCopy(in, out);
			} else {
				IOUtils.copy(in, out);
			}
			IOUtils.closeQuietly(out);
			return tempFile;
		} catch (IOException e) {
			log.error("getResourceAsFile(file) -> Failed to read resource!", e);
			return null;
		}
	}

	public static String getResourceAsString(final Class<?> cls, final String resourcePath) {
		Variables.requireNonNull(cls, "cls");
		Variables.requireNonNull(resourcePath, "resourcePath");
		try {
			return new String(getResourceBytes(cls, resourcePath));
		} catch (Exception e) {
			log.error("getResourceAsString(file) -> Failed to read resource!", e);
			return null;
		}
	}

	public static String getFileExtension(final File file) {
		Variables.requireNonNull(file, "file");
		String name = file.getName();
		try {
			return name.substring(name.lastIndexOf(".") + 1);
		} catch (Exception e) {
			return "";
		}
	}

	public static String getFileStringExtension(final String name) {
		Variables.requireNonNull(name, "name");
		try {
			return name.substring(name.lastIndexOf(".") + 1);
		} catch (Exception e) {
			return "";
		}
	}

	public static long getSize(final File file) {
		Variables.requireNonNull(file, "file");
		long size = 0;

		if (file.isDirectory()) {
			File[] files = file.listFiles();
			for (int i = 0; i < files.length; i++) {
				size += getSize(files[i]);
			}
		} else {
			size += file.length();
		}

		return size;
	}

	/*
	 * Proudly copied, updated and stolen from:
	 * https://programming.guide/worlds-most-copied-so-snippet.html
	 */

	public static strictfp String convertToStringRepresentation(final long bytes) {
		return convertToStringRepresentation(bytes, false);
	}

	public static strictfp String convertToStringRepresentation(long bytes, final boolean si) {
		final int unit = si ? 1000 : 1024;
		final long absBytes = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);
		if (absBytes < unit)
			return bytes + " B";
		int exp = (int) (Math.log(absBytes) / Math.log(unit));
		final long th = (long) Math.ceil(Math.pow(unit, exp) * (unit - 0.05));
		if (exp < 6 && absBytes >= th - ((th & 0xFFF) == 0xD00 ? 51 : 0))
			exp++;
		final String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
		if (exp > 4) {
			bytes /= unit;
			exp -= 1;
		}
		return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
	}

	/* */

	public static String readFirstLine(final File file) throws Exception {
		Variables.requireNonNull(file, "file");
		String data = "";
		if (file.isDirectory())
			throw new Exception("File is a directory.");
		final InputStream fis = new FileInputStream(file);
		final InputStreamReader isr = new InputStreamReader(fis, Charset.forName("UTF-8"));
		final BufferedReader br = new BufferedReader(isr, IOUtils.MEMORY_ALLOC_BUFFER);
		String buffer;
		while ((buffer = br.readLine()) != null) {
			data = buffer;
			break;
		}
		try {
			fis.close();
			isr.close();
			br.close();
		} catch (Exception e) {
		}
		return data;
	}

	public static String getFileName(String filePath) {
		if (filePath == null || filePath.length() == 0)
			return "";
		filePath = filePath.replaceAll("[/\\\\]+", "/");
		int len = filePath.length(), upCount = 0;
		while (len > 0) {
			// remove trailing separator
			if (filePath.charAt(len - 1) == '/') {
				len--;
				if (len == 0)
					return "";
			}
			int lastInd = filePath.lastIndexOf('/', len - 1);
			String fileName = filePath.substring(lastInd + 1, len);
			if (fileName.equals(".")) {
				len--;
			} else if (fileName.equals("..")) {
				len -= 2;
				upCount++;
			} else {
				if (upCount == 0)
					return fileName;
				upCount--;
				len -= fileName.length();
			}
		}
		return "";
	}

	public static List<File> listFilesForFolder(final File folder) {
		Variables.requireNonNull(folder, "folder");
		if (folder == null) {
			log.error("listFilesForFolder(folder) -> Input was null!");
			return Lists.of();
		}
		if (folder.listFiles() == null) {
			log.error("listFilesForFolder(folder) -> Input is a file!");
			return Lists.of();
		}
		final List<File> files = FastLists.list();
		for (final File fileEntry : folder.listFiles()) {
			if (fileEntry.isDirectory()) {
				files.addAll(listFilesForFolder(fileEntry));
				files.add(fileEntry);
			} else {
				files.add(fileEntry);
			}
		}
		return files;
	}

	public static byte[] readFileToBytes(final File file) throws Exception {
		Variables.requireNonNull(file, "file");
		final FileInputStream fis = new FileInputStream(file);
		final byte[] arr = IOUtils.toByteArray(fis);
		IOUtils.closeQuietly(fis);
		return arr;
	}

	public static Path pathOf(final String value) {
		return Paths.get(value);
	}
}
